{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\n",
    "# 实践方法论\n",
    "\n",
    "&emsp;&emsp;机器学习实践当中，除了了解各种机器学习算法和技术，并解释算法的原理外，更重要的是能正确使用一个算法，具体包括是否收集更多的数据、增加或减少模型容量、增加或删除正则化、改进模型的优化、改进模型的近似推断或调试模型的软件实现等等。这些操作都需要大量尝试和时间，因此需要掌握一些简单的方法论。\n",
    "\n",
    "&emsp;&emsp;建议参考以下的流程：\n",
    "\n",
    "- 确定目标：根据问题的特点，确定使用什么误差度量。\n",
    "- 尽快建立端到端的工作流程(pipeline)，包括估计合适的性能度量。\n",
    "- 搭建系统，确定性能瓶颈并分析性能不佳的主要原因。\n",
    "- 根据具体原因反复进行改进，如收集更多数据、调整超参数及改进算法等。\n",
    "\n",
    "\n",
    "## 1. 性能度量\n",
    "\n",
    "&emsp;&emsp;确定误差度量将指导接下来的所有工作，是我们实践的第一步。后面的工作都将围绕提高性能度量展开。\n",
    "\n",
    "&emsp;&emsp;如何确定合理的性能期望？学术界的通常做法是，根据先前研究成果的基准结果来估计预期错误率。而在商业实践中，需要考虑安全稳定、成本效益和对于客户的吸引力等因素。\n",
    "\n",
    "&emsp;&emsp;除性能度量外，另一个需要考虑的是具体度量的选择。最简单的度量包括**准确率**、**错误率**等，更多应用中需要根据不同的应用场景可以选择更复杂度量。\n",
    "\n",
    "&emsp;&emsp;比如,有时一种错误比另一种错误的代价更加严重——相较于将垃圾邮件错认为正常邮件，将正常邮件错误归为垃圾邮件对用户带来的麻烦更大，这种情况下不能简单度量垃圾邮件分类的错误率，而需要将拦截正常邮件的代价设置的更高。\n",
    "\n",
    "&emsp;&emsp;再比如有时需要训练检测罕见事件的二分类器——对于患病率为百万分之一的疾病的检测，分类器只需一直给出有没患病的结果，其正确率就能达到99.9999%，正确率很难描述这种系统的性能，通常通过度量**精度(precision)**和**召回率(recall)**，用y轴表示精度，x轴表示召回率，画出**PR曲线(PR curve)**观察分类器的性能。很多情况下需要用一个数量化分类器的性能，这是可以用PR曲线下方总面积表示分类器性能，或可以将精度(p)和召回率(r)转换为**F分数(F-score)**\n",
    " $$\\frac{2pr}{p+r}$$\n",
    "\n",
    "&emsp;&emsp;一些应用中错误判断会导致较大危害，如Google街景转录系统如果将地址识别错误，将极大影响用户的使用体验，从而导致地图应用的价值严重下降，这时需要算法能够估计所作判断的置信度，在系统认为不能正确转录时，交由人工方法转录街景照片。这种情况下，一个重要的性能度量是**覆盖(coverage)**，我们可以权衡覆盖和精度两个指标，让系统保证一定准确度的前提下尽可能减少人工操作的工作量。\n",
    "\n",
    "&emsp;&emsp;在其他专业的应用领域中，还有更多其他性能度量指标或者特定标准，如**点击率**，**用户满意度**等等。\n",
    "\n",
    "\n",
    "## 2. 默认的基准模型\n",
    "\n",
    "&emsp;&emsp;为方便尽快建立合理的端到端系统，本节为不同情况下使用哪种算法给出了推荐。\n",
    "\n",
    "+ 视问题的复杂度而言，如果只需正确地选择几个线性权重就可以解决问题，那么无需使用深度学习，可以从一个简单的统计模型开始，如逻辑回归。\n",
    "\n",
    "+ “AI-完全”([AI-complete](https://en.wikipedia.org/wiki/AI-complete))类问题，如对象识别、语音识别、机器翻译等，开始于合适的深度学习模型效果会比较好。\n",
    "\n",
    "+ 根据数据的结构选择模型。\n",
    "  + 如果项目是以固定大小的向量作为输入的监督学习，那么可以使用全连接的前馈网络。\n",
    "  + 如果输入有已知的拓扑结构（如图像），那么可以使用卷积网络，可以开始于使用某些分段线性单元，如ReLU或者其拓展（如Leaky ReLU，PReLU和maxout）。\n",
    "  + 如果输入或输出是一个序列，可以使用门控循环网络（LSTM或GRU）。\n",
    "\n",
    "+ 优化算法时，可以考虑使用：\n",
    "  + 具有衰减学习率以及动量的SGD（衰减到固定的最低学习率的线性衰减、指数衰减，或每次发生验证错误停滞时将学习率减低2-10倍等）。\n",
    "  + 批标准化（batch normalization），对卷积网络和具有sigmoid非线性函数的网络的性能优化效果显著。\n",
    "  + Adam算法。\n",
    "  \n",
    "+ 除非训练集样本量足够大，否则项目应该在一开始加入一些温和的正则化，如Dropout既是一种容易实现、且兼容很多模型和算法的正则化项（批标准化也能将低泛化误差，此时可以省略Dropout，因为用于标准化变量的统计量估计本身就存在噪声）。\n",
    "\n",
    "+ 所训练任务与现有被广泛研究的任务相似时，可以复制已有研究中效果良好的模型和算法。甚至可以从已有研究中复制一个训练好的模型。最常见的情况是，计算机视觉任务可以使用imageNet上训练好的卷积网络的特征。\n",
    "\n",
    "+ 一些特定领域很适合使用无监督学习技术，如自然语言处理领域大大受益于学习无监督词嵌入等方法。而一些领域目前并不适用与无监督学习方法，如计算机视觉。\n",
    "\n",
    "## 3. 是否收集更多数据\n",
    "\n",
    "&emsp;&emsp;收集更多数据往往比改进学习算法有用得多。\n",
    "\n",
    "&emsp;&emsp;首先怎么判断是否要收集更多的数据？\n",
    "\n",
    "+ 训练集上的性能是否可接受。如果训练集上的性能就很差，那就没有必要收集更多的数据了。这时可以尝试增加更多的网络层或每层增加更多的隐藏单元，以增加模型的规模。也可以调整学习率等超参数来改进学习算法。如果更大的模型和仔细调整的优化算法效果不佳，那么问题可能源自训练数据的质量，需要从头开始收集干净的特征更加丰富的数据。\n",
    "+ 如果训练集上的性能是可接受的，而测试集上的性能比训练集的要差很多，那么收集更多的数据是最有效的解决方案之一。这是需要考虑的是收集更多数据的代价和可行性与新增数据对模型性能提升的显著性。\n",
    "  + 对于大型网络公司而言，其庞大的用户储备使得收集数据集简单可行，收集更多数据往往是不二之选。\n",
    "  + 如医疗应用等情况下，收集更多数据的代价通常很高，甚至完全没有可行性，这时一个可行的简单方法是降低模型大小或是改进正则化（调整超参数，如权重衰减系数、或是加入Dropout等正则策略）。如果调整正则化超参数后，训练集性能和测试集性能之间的差距还是不可接受，那么还得想办法考虑收集更多数据。\n",
    "\n",
    "&emsp;&emsp;其次，应该再收集多少数据？\n",
    "+ 通过绘制曲线显示训练集规模和泛化误差之间的关系，根据走势延伸曲线可以预测还需要多少训练数据。\n",
    "  + 通常，加入小数量的样本不会对泛化误差产生显著的影响，一般建议在对数级尺度上增加训练集的大小（如倍增样本数目）。\n",
    "\n",
    "&emsp;&emsp;**如果收集更多的数据还是不能有效提升系统的性能，那么改进的唯一方法是改进学习算法本身。**\n",
    "\n",
    "## 4. 选择超参数\n",
    "\n",
    "&emsp;&emsp;有些超参数影响算法运行的时间和空间开销，有的影响学习的模型质量，有的影响模型在新数据上的推理能力。大部分深度学习算法都可以通过超参数来控制算法在不同方面的表现。**调整超参数的主要目标是让模型的有效容量和任务的复杂度相匹配。**\n",
    "\n",
    "&emsp;&emsp;两种选择超参数的基本方法：\n",
    "+ 手动选择：需要了解超参数做了些什么，以及机器学习模型如何才能取得良好的性能。\n",
    "+ 自动选择：无需深入了解算法及模型细节，但往往需要更高的计算开销。\n",
    "\n",
    "#### 4.1 手动调整超参数\n",
    "\n",
    "&emsp;&emsp;手动调整超参数主要通过判断是否过拟合或欠拟合，调整超参数增加或者减少模型容量来进行。\n",
    "\n",
    "&emsp;&emsp;以某个超参数为自变量，以泛化误差为因变量，绘制函数曲线，则图线通常会表现为U形曲线。一种极端情况下，超参数的值意味着低容量，泛化误差由于训练误差较大而提高，即欠拟合；另一种极端情况下，超参数的值对应着高容量，泛化误差由于训练误差与测试误差之间的差距较大而提高。某些超参数数值太大容易过拟合，如中间层隐藏单元的数量；某些超参数数值太小容易过拟合，如权重衰减系数。\n",
    "\n",
    "|超参数|如何增加模型容量|原因|注意事项 |\n",
    "|:-|:-|:-|:-|\n",
    "|隐藏单元数量|增加|增加隐藏单元数量会增加模型的表示能力|几乎模型每个操作所需的时间和空间成本都会随隐藏单元数量的增加而增加|\n",
    "|学习率|调至最优|不正确的学习率（太高或是太低）都会导致模型有效容量降低||\n",
    "|卷积核宽度|增加|增加卷积核宽度会增加模型的参数数量|较宽的卷积核会导致较窄的输出尺寸，除非使用隐式零填充减少此影响，否则会导致降低模型容量。较宽的卷积核会增加时间、空间开销，而另一方面，较窄的输出会降低内存代价|\n",
    "|隐式零填充|增加|在卷积之前隐式添加零能保持较大尺寸的表示|大多数操作的时间和内存代价会增加|\n",
    "|权重衰减系数|降低|降低权重衰减系数使得模型参数可以自由地增大||\n",
    "|Dropout比率|降低|较少地丢弃单元可以更多地让单元彼此“协力”适应训练集| | |\n",
    "<center>各种超参数对模型容量的影响表</center>\n",
    "\n",
    "&emsp;&emsp;并非所有超参数都能对应完整的U形曲线，比如有些超参数是离散的（中间层单元数目、maxout单元中线性元件的数目）、有上下界的，甚至是二值的（一般用于控制是否有某功能），这种情况只能沿曲线探索一些点。\n",
    "\n",
    "&emsp;&emsp;**学习率**是最重要的超参数。如果时间有限，那就重点调整学习率。学习率关于训练误差的一个典型U形曲线如图所示。学习率太大，训练误差会随学习率增加而迅速增大；学习率过小，则会导致训练过程很慢，并可能永久停留字啊一个较高的训练误差水平。\n",
    "\n",
    "&emsp;&emsp;本例中不同学习率取值下，模型的accuracy情况如下图所示：\n",
    "\n",
    "![6e-2](../img/6e-2.png)<center>learning_rate=6e-2</center>\n",
    "![4e-2](../img/4e-2.png)<center>learning_rate=4e-2</center>\n",
    "![3e-2](../img/3e-2.png)<center>learning_rate=3e-2</center>\n",
    "![2e-2](../img/2e-2.png)<center>learning_rate=2e-2</center>\n",
    "![7e-3](../img/7e-3.png)<center>learning_rate=7e-3</center>\n",
    "![5e-3](../img/5e-3.png)<center>learning_rate=5e-3</center>\n",
    "![1e-3](../img/1e-3.png)<center>learning_rate=1e-3</center>\n",
    "![5e-4](../img/5e-4.png)<center>learning_rate=5e-4</center>\n",
    "\n",
    "&emsp;&emsp;调整学习率以外的其他参数时，需要同时监测训练误差和测试误差，以判断是否过拟合或欠拟合，并据此针对性调整超参数。如果测试集错误率过大，根据$Error_{test} = (Error_{test}-Error_{train})+Error_{train}$，可以采取以下两种方法：\n",
    "  + 如果训练集错误率过大，那么只能向增加模型容量的方向调整超参数。如添加更多的网络层或隐藏单元。\n",
    "  + 训练误差较小（也即模型容量足够大）时，关键在于缩小测试误差与训练误差之间的差距，可以改变正则化超参数适当较小有效模型容量。如增加Dropout或权重衰减策略等。\n",
    "（另**加入正则项只是提升测试集性能的一种方法。在训练误差低的情况下，完全可以通过收集更多的训练集来减少泛化误差。**）\n",
    "\n",
    "#### 4.2 自动超参数优化\n",
    "\n",
    "&emsp;&emsp;某种程度上，一些算法流行的原因在于其只较少的参数需要调整便能表现出不错的性能，如逻辑斯提回归、支持向量机。\n",
    "\n",
    "&emsp;&emsp;考虑手动调参的过程，其本质是一种优化任务：寻找超参数来优化目标函数，有时还有时间、空间开销作为约束条件。因而原则上可能开发出封装学习算法的超参数优化算法。虽然这一类算法本身有自己的超参数（每个超参数应该被探索的值域），好在这一类次级超参数往往更容易选择，也即调整次级超参数更可能有更好的性能。\n",
    "\n",
    "  ###### 4.2.1 网格搜索(grid search)\n",
    "  \n",
    "&emsp;&emsp;当有三个或更少的超参数时，常使用网格搜索。具体做法是：为每个超参数选择数量较小的参数集（离散化），这些超参数的笛卡尔积作为待搜索的超参数组集，使用每组超参数训练模型，挑选验证误差最小的超参数组作为最好的超参数。通常会设置不同的超参数组集运行多次网格搜索，基于前一次运行结果改进下一次搜索。\n",
    "\n",
    "&emsp;&emsp;网格搜索存在的显著问题是：随着超参数数量的增长，搜索的计算成本呈指数级增长。另外，有的模型中只有一个或少数超参数对结果产生重要影响，这种情况下，网格搜索会进行很多无用的探索。\n",
    "\n",
    "  ###### 4.2.2 随机搜索(random search)\n",
    "  \n",
    "&emsp;&emsp;随机搜索为每个超参数定义一个边缘分布，而不需要将超参数离散化。这样可以在更大的集合范围内进行搜索。\n",
    "\n",
    "&emsp;&emsp;相较于网格搜索，随机搜索更容易实现，且能更快收敛。网格搜索中探索一个超参数的不同值时，其他超参数取值是一样的，因而可能出现多次探索结果相同的情况，而随机搜索时其他参数也会有不同的值，因此不会浪费搜索资源。\n",
    "![网格搜索与随机搜索的对比](../img/grid search VS random search.png)\n",
    "<center>网格搜索与随机搜索的对比,截图自Goodfellow I, Bengio Y, Courville A. Deep Learning[M]. The MIT Press, 2016:369</center>\n",
    "\n",
    "  ###### 4.2.3 基于模型的参数优化\n",
    "  \n",
    "&emsp;&emsp;超参数的调参可以转化为一个优化问题，待调整的超参数是决策变量，模型的验证误差是优化代价。这类问题的直接解决方法是，简化为计算验证集上误差函数关于超参数的梯度，然后遵循这个梯度优化更新。遗憾的是，多数情况下这个方法不可用（原因包括时间、空间开销不可接受，或超参数本质上不可导），这时可以对模型的验证误差建模，通过优化验证误差的模型达到优化超参数的目的。\n",
    "\n",
    "&emsp;&emsp;现有大多数基于模型的超参数搜索算法都使用贝叶斯回归模型，这一类方法目前还不够成熟可靠，有时可能得到很好的效果，但有时可能产生灾难性的失误。关于超参数优化的前沿方法还有[Spearmint](http://papers.nips.cc/paper/4522-practical-bayesian-optimization-of-machine-learning-algorithms), [TPE](http://papers.nips.cc/paper/4443-algorithms-for-hyper-parameter-optimization), [SMAC](https://link.springer.com/chapter/10.1007%2F978-3-642-25566-3_40)。\n",
    "\n",
    "## 5. 调试策略\n",
    "\n",
    "&emsp;&emsp;当一个机器学习系统效果不好时，很难判断是算法本身的问题，还是算法实现中出现错误。本节给出一些重要的调试检测方法：\n",
    "\n",
    "- **可视化运行中的模型的行为**：如检测图像的模型，可以查看模型检测图像的结果；语音生成的模型，试听生成的语音样本等。\n",
    "- **可视化最严重的错误**：大多数模型能够输出运行任务时的某种置信度量（如softmax函数输出层的分类器给每个类分配一个概率，因此分配给最有可能的类的概率即模型的置信估计值），基于置信度，可以观察训练集中最难正确建模的样本，这样可能发现该数据预处理或标记方式方面的问题。\n",
    "- **根据训练和测试误差检测软件**：当训练误差较低而测试误差较高时，可能由于算法原因过拟合，也有可能是因为模型重载出现问题，还可能因为测试数据和训练数据处理方式不同。当训练误差和测试误差都很高时，为判断是软件错误还是算法原因欠拟合，需要进一步测试：\n",
    "  + **拟合极小的数据集**：假设训练得到的分类器不能正确标注一个单独的样本，那么很可能是存在软件错误。这种方法一般用于只有少量样本的小数据集上。\n",
    "  + **可视化监控激活函数值和梯度**：将神经网络在大量训练迭代中收集到的激活函数值和梯度的统计量进行可视化展示。隐藏单元的预激活值可以告诉我们该单元是否饱和，及单元饱和的频率如何；整流器多久关一次，是否有单元一直关闭；双曲正切单元，预激活绝对值的平均值可以告诉我们该单元的饱和程度；深度网络中，传播梯度的快速增长或快速消失可能会阻碍优化过程等等。观察参数梯度和参数的量级也很有用，一般情况下参数在一个小批量更新中变化的理想幅度大概是参数量值的1%尺度。另外，如果数据稀疏（如自然语言），则有些参数可能很少更新，检测其变化时应该注意。\n",
    "  + **比较反向传播导数和数值导数**：在需要定义bprop方法时的情况下，常见的错误原因是没能正确地实现梯度表达。一种验证其正确性的方法是，比较所实现的自动化求导和通过有限差分（finite difference）计算的导数:\n",
    "  $$f^\\prime (x)=\\lim_{\\epsilon \\to 0} \\frac{f(x+\\epsilon)-f(x)}{\\epsilon}$$\n",
    "  求倒数时使用很小的、有限的$\\epsilon$进行差分计算估算导数值，比如$\\epsilon=10^{-150}$:\n",
    "  $$f^\\prime (x)\\approx \\frac{f(x+\\epsilon)-f(x)}{\\epsilon}$$\n",
    "  可以通过中心差分（centered difference）提高近似的准确率（公式）:\n",
    "  $$f^\\prime (x)\\approx \\frac{f(x+\\frac {1}{2}\\epsilon)-f(x-\\frac {1}{2}\\epsilon)}{\\epsilon}$$\n",
    "如果可以在复数上进行计算，那么使用复数的$\\epsilon$，这样可以更高效的计算导数估算梯度:\n",
    "$$f(x+i\\epsilon)=f(x) + i\\epsilon f^\\prime (x)+O(\\epsilon^2)$$\n",
    "$$real(f(x+i\\epsilon))=f(x)+O(\\epsilon^2), image(\\frac{f(x+i\\epsilon)}{\\epsilon})=f^\\prime (x)+O(\\epsilon^2)$$\n"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
